Skip to content

[SEA-NodeJS] (1/8) Backend abstraction — IBackend / ISessionBackend / IOperationBackend#378

Draft
msrathore-db wants to merge 3 commits into
mainfrom
msrathore/sea-abstraction
Draft

[SEA-NodeJS] (1/8) Backend abstraction — IBackend / ISessionBackend / IOperationBackend#378
msrathore-db wants to merge 3 commits into
mainfrom
msrathore/sea-abstraction

Conversation

@msrathore-db
Copy link
Copy Markdown
Contributor

@msrathore-db msrathore-db commented May 16, 2026

Stack

Linear stack of 8 PRs landing the M0 + M1 Phase 1 SEA NodeJS work. Merge in order from base ↑ to tip. The tip branch (msrathore/sea-auth-u2m, PR #383) is the single snapshot containing everything in flight — point your test or benchmark harness at it for an end-to-end check.

Pos PR Branch Scope
1/8 #378 sea-abstraction IBackend / ISessionBackend / IOperationBackend interfaces
2/8 #380 sea-napi-binding TS loader + build script for the kernel-provided .node artifact
3/8 #377 sea-errors-logging Kernel ErrorCode → JS error-class mapping (M0 minimum)
4/8 #379 sea-auth PAT auth via useSEA: true
5/8 #382 sea-execution executeStatement + openSession (sessionConfig, initialCatalog/Schema)
6/8 #381 sea-results CloudFetch + Inline Arrow result fetching
7/8 #384 sea-operation cancel / close / finished lifecycle + INTERVAL parity + napi-relocation acceptance (absorbed sea-integration content)
8/8 #383 sea-auth-u2m ← TIP M1 Phase 1 OAuth M2M + U2M (5 review rounds, ZERO HIGH at close)

Companion kernel stack (databricks/databricks-sql-kernel): 8 PRs — root #26 (async-public-api) → #27#25#29#28#30#24#23 (tip).

Policy: new PRs always stack on the current tip. No sibling/parallel topology. No force-pushes on existing PRs unless absolutely necessary; if a PR's content is wrong, add a fix-up commit on top of the stack tip rather than rewriting history.


This PR is position 1/8 (ROOT of the stack).

Summary

Foundational backend abstraction — introduces IBackend / ISessionBackend / IOperationBackend interfaces and refactors the existing thrift driver to plug in behind them.

Size note (1839 LOC, over the 800 cap) — foundational refactor

+1064/-775 ratio means ~775 lines are pre-existing code being refactored to plug into the new interfaces (renames, restructuring, no behavior change). Actual review burden is lower than the raw LOC suggests — most of the diff is mechanical file-level refactoring rather than new logic.

Cannot be cleanly split: interfaces + at least one implementation (thrift) must land together to compile.

Test plan

  • ✅ Existing thrift backend wraps cleanly behind IBackend interface
  • ✅ No behavior change on the thrift path
  • ✅ Stub SeaBackend compiles + exposes the same interface

Draft until you give the go for review.

Tracking

  • PECOBLR-2666 — NodeJS SEA integration (parent epic — backend abstraction scaffolding)

Co-authored-by: Isaac

…kend

Refactors DBSQLClient/Session/Operation to dispatch through three
backend interfaces. ThriftBackend (lib/thrift-backend/) contains the
relocated existing thrift logic. SeaBackend (lib/sea/) is a stub for
M0; the sea-napi-binding feature wires the real impl.

Public surface (lib/index.ts) unchanged.
No new dependencies. All existing tests pass.

Files:
- lib/contracts/IBackend.ts (new)
- lib/contracts/ISessionBackend.ts (new)
- lib/contracts/IOperationBackend.ts (new)
- lib/contracts/IDBSQLClient.ts (adds useSEA?: boolean to ConnectionOptions)
- lib/thrift-backend/ThriftBackend.ts (new)
- lib/thrift-backend/ThriftSessionBackend.ts (new)
- lib/thrift-backend/ThriftOperationBackend.ts (new)
- lib/sea/SeaBackend.ts (new, M0 stub)
- lib/DBSQLClient.ts (dispatch through IBackend; useSEA picks SeaBackend)
- lib/DBSQLSession.ts (facade over ISessionBackend; staging stays here)
- lib/DBSQLOperation.ts (facade over IOperationBackend; iterators/fetchAll stay here)
- tests/unit/DBSQLClient.test.ts (retarget internal state lookup through backend; pre-seed client.backend in tests that bypass connect())
- tests/unit/DBSQLOperation.test.ts (retarget internal state lookup through backend)
…nline type

Addresses code-bloat-watchdog findings from commit 0085928:
- Restores public-API JSDoc on DBSQLSession + DBSQLOperation methods
  (was deleted as scope creep; contracts unchanged so docs still apply)
- Adds makeStubbedClient() helper to tests/unit/DBSQLClient.test.ts;
  replaces 14× duplicated ThriftBackend pre-seed
- Imports WaitUntilReadyOptions instead of inline option types in
  IOperationBackend + DBSQLOperation.waitUntilReady
@github-actions
Copy link
Copy Markdown

Thanks for your contribution! To satisfy the DCO policy in our contributing guide every commit message must include a sign-off message. One or more of your commits is missing this message. You can reword previous commit messages with an interactive rebase (git rebase -i main).

Round-N fixes from the 9-reviewer pre-review. Public IOperation/DBSQLOperation
surface preserved byte-identical; backend interfaces (IBackend / ISessionBackend
/ IOperationBackend) made fully neutral so both Thrift and SEA can implement
the same contract.

F1 — neutral DTOs at IOperationBackend with Thrift-shape preservation on the
  public facade (adapter pattern):
- lib/contracts/OperationStatus.ts (new) — neutral OperationStatus + OperationState
  enum mirroring databricks-sql-python's CommandState and kernel pyo3's
  StatementStatus taxonomy.
- lib/contracts/ResultMetadata.ts (new) — neutral ResultMetadata + ResultFormat
  enum mirroring the three TSparkRowSetType cases.
- IOperationBackend.status()/getResultMetadata() return the neutral DTOs.
- ThriftOperationBackend.status() adapts at the boundary via adaptOperationStatus
  / adaptResultMetadata; module-level helpers thriftStateToOperationState and
  thriftRowSetTypeToResultFormat do the enum maps.
- ThriftOperationBackend exposes thriftStatusResponse() and
  thriftResultMetadataResponse() as public Thrift-only accessors used by the
  facade's zero-loss fast path (kept for internal state-machine + result-handler
  dispatch as well).
- lib/utils/thriftWireSynthesis.ts (new) — synthesizeThriftStatus and
  synthesizeThriftResultSetMetadata: convert neutral DTOs back to Thrift wire
  shape for the non-Thrift backend path. Lossy on Thrift-only fields
  (taskStatus, numModifiedRows, cacheLookupResult, etc.).
- DBSQLOperation.status() and getMetadata() preserved Thrift return shape:
  Thrift backend path returns the real wire response (zero loss); non-Thrift
  backend path synthesizes via the new helpers.
- DBSQLOperation.getResultMetadata() — new additive neutral accessor on
  IOperation; DBSQLSession.handleStagingOperation uses it instead of the
  deprecated Thrift-shaped getMetadata().

F2 — IBackend.connect() is now zero-arg. Backend reads everything it needs
  from IClientContext / constructor; matches Python connector's pattern of
  passing session_configuration via constructor not method-arg.

F3 — Restore the 'Server protocol version' debug log dropped by the original
  PR-378 refactor. Re-added to ThriftSessionBackend.constructor with the
  LogLevel.debug + IClientContext.getLogger() pattern; matches the pre-refactor
  log site at main:lib/DBSQLSession.ts:175.

F4 + F11 + F14 — SeaBackend stub safety:
- close() is a no-op so DBSQLClient.close()'s state-clearing block can finish
  even after a useSEA: true connect() failure.
- connect() and openSession() throw HiveDriverError instead of generic Error,
  matching the rest of the codebase.
- connect(options: ConnectionOptions) and openSession(request: OpenSessionRequest)
  declare their parameters (with @typescript-eslint/no-unused-vars disable)
  so IDE autocomplete prompts the M1 SEA implementer.

F6 + F7 + F9 + F10 — JSDoc on backend interfaces:
- IBackend: connect/openSession/close docstrings; close() doc explicitly
  states transport-layer cleanup is owned by DBSQLClient.
- ISessionBackend: copy IDBSQLSession's per-method one-liner JSDoc.
- IOperationBackend: doc hasResultSet (readonly external; mutates internally),
  waitUntilReady (MUST throw OperationStateError on terminal non-success).

F8 — tests/unit/sea/SeaBackend.test.ts (new) locks in the stub contract:
  connect() rejects HiveDriverError, openSession() rejects HiveDriverError,
  close() resolves no-op. ~30 LOC.

F12 — Drop legacy { handle, ... } ctor branch from DBSQLOperation and
  DBSQLSession:
- Facades accept only { backend, context }.
- DBSQLSession no longer imports ThriftSessionBackend at all.
- DBSQLOperation imports ThriftOperationBackend solely for the F1 typed
  downcast (zero-loss Thrift fast path); this is a deliberate, scoped
  coupling tied to the back-compat decision.
- tests/unit/.stubs/createSessionForTest.ts and createOperationForTest.ts
  (new) wrap the legacy shape; all 48 + 54 test sites mechanically migrated.

F15 — ThriftOperationBackend.waitUntilReady uses imported WaitUntilReadyOptions
  type instead of an anonymous inline shape.

F16 — useSEA flag moved out of public ConnectionOptions:
- Removed useSEA?: boolean from the exported lib/contracts/IDBSQLClient.ts
  ConnectionOptions; no longer ships in the public .d.ts.
- lib/contracts/InternalConnectionOptions.ts (new) declares the flag as a
  non-exported internal extension; DBSQLClient.connect() reads via a typed
  cast. Mirrors Python's kwargs.get('use_sea', False) pattern at
  databricks-sql-python/src/databricks/sql/session.py:111.

F17 — Missing return; after case 'timeout' in forwardConnectionEvent so a
  future fifth case doesn't silently fall through. The trailing return; in
  the last case triggers no-useless-return — quieted with a localized
  eslint-disable-next-line + intent comment.

F5 — deferred per owner instruction (test-only as any cast tightening).

Verification:
- yarn lint clean (3 pre-existing warnings in tests/e2e/protocol_versions.test.ts).
- yarn build clean.
- tsc --noEmit -p tsconfig.json clean (apart from pre-existing
  examples/tokenFederation/* import errors that exist on main).
- Runtime smoke test of SeaBackend stub + Thrift-wire synthesis round-trip
  passes 5/5 assertions.

Co-authored-by: Isaac
Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
msrathore-db added a commit that referenced this pull request May 24, 2026
Addresses PR #380 review findings C1, C2 (partial), C3, H1, H2, H3,
H4, H5, H6, H7, H8 (runtime guard), H9, M1, M2, M3, M4 (linguist),
M5, M6, M7, M8, L1, L2, L4.

- C1 lint: drop `.js` ext from native/sea require so eslint
  import/extensions passes; gitignore the auto-generated index.js
  and *.node artifacts; prettier-ignore the napi-rs auto-generated
  index.d.ts / index.js.
- C2 publish: whitelist native/sea/index.{js,d.ts} in .npmignore;
  declare @databricks/sea-native-linux-x64-gnu in optionalDependencies.
  The kernel-side package-name rename (drop the linux-x64-gnu
  prefix) is tracked separately as a cross-PR ask.
- C3 test wiring: move tests/native/version.test.ts ->
  tests/unit/sea/, tests/native/e2e-smoke.test.ts -> tests/e2e/sea/;
  both now picked up by the existing mocharc globs and run via the
  existing `npm test` / `npm run e2e` jobs.
- H1 noise drop: version.test.ts now asserts one meaningful semver
  check; three tautological assertions removed.
- H2 + H3 + H7 lazy load: rewrite SeaNativeLoader.ts on the
  lib/utils/lz4.ts pattern. Lazy require behind getSeaNative();
  capability-detection helper tryGetSeaNative(); structured error
  messages that classify MODULE_NOT_FOUND vs ERR_DLOPEN_FAILED and
  include platform/arch/Node-version + install hint.
- H4 supply chain: pin @napi-rs/cli to 2.18.4 in devDependencies;
  build:native switches to `npx --no-install` so the pinned local
  install is used (no per-build network fetch).
- H5 path: switch build:native default kernel path to the canonical
  `../../databricks-sql-kernel/napi-binding` (the worktree-specific
  `-sea-WT` suffix is gone).
- H6 CI safety: e2e suite hard-fails when CI=true and any required
  env var is missing; dev machines still skip.
- H8 runtime guard: loader throws a structured error on Node <18
  instead of attempting a dlopen that would fail mysteriously.
  Driver's engines.node stays >=14 — Thrift consumers on older
  Node continue to work.
- H9 + M1 + M2 types: tsconfig adds a `@sea-native` path alias to
  native/sea/index.d.ts; loader imports the real Connection /
  Statement / ConnectionOptions / ExecuteOptions / ArrowBatch /
  ArrowSchema types and re-exports them. Tests use the re-exported
  types — the inline-shape duplication across three files collapses.
- M3 README: rewrite native/sea/README.md to match kernel reality.
  The napi crate is a standalone Cargo workspace (not a pyo3
  sibling); explain the tls-rustls choice that the standalone
  workspace exists to enable.
- M4 drift detection: mark native/sea/index.d.ts as
  linguist-generated in .gitattributes so GitHub collapses it in
  diffs and excludes from blame/language stats.
- M5 artifacts: gitignore native/sea/index.js, index.node,
  index.*.node.
- M6 + M7 e2e coverage: decode the IPC payload via apache-arrow's
  tableFromIPC and assert numRows + cell value (not just shape);
  add drain-past-null idempotence and schema-before-fetch
  coverage.
- L1 build scripts: collapse build:native + build:native:debug
  into one script taking BUILD_PROFILE (defaults to --release).
- L2 / L4 / M8: drop the version() alias and the "Round 2+ will…"
  comment debt; the loader now actually delivers the value the
  prior JSDoc was only promising.

Verified on this branch: npm run lint clean (0 errors); npm run
prettier clean for PR-owned files (3 unrelated pre-existing
warnings on PR #378 territory); tsc --project tsconfig.build.json
clean; mocha on tests/unit/sea passes (4/4); mocha on
tests/e2e/sea passes against a live pecotesting warehouse (2/2,
IPC decode confirms SELECT 1 returns 1).
msrathore-db added a commit that referenced this pull request May 24, 2026
Addresses PR #380 review findings C1, C2 (partial), C3, H1, H2, H3,
H4, H5, H6, H7, H8 (runtime guard), H9, M1, M2, M3, M4 (linguist),
M5, M6, M7, M8, L1, L2, L4.

- C1 lint: drop `.js` ext from native/sea require so eslint
  import/extensions passes; gitignore the auto-generated index.js
  and *.node artifacts; prettier-ignore the napi-rs auto-generated
  index.d.ts / index.js.
- C2 publish: whitelist native/sea/index.{js,d.ts} in .npmignore;
  declare @databricks/sea-native-linux-x64-gnu in optionalDependencies.
  The kernel-side package-name rename (drop the linux-x64-gnu
  prefix) is tracked separately as a cross-PR ask.
- C3 test wiring: move tests/native/version.test.ts ->
  tests/unit/sea/, tests/native/e2e-smoke.test.ts -> tests/e2e/sea/;
  both now picked up by the existing mocharc globs and run via the
  existing `npm test` / `npm run e2e` jobs.
- H1 noise drop: version.test.ts now asserts one meaningful semver
  check; three tautological assertions removed.
- H2 + H3 + H7 lazy load: rewrite SeaNativeLoader.ts on the
  lib/utils/lz4.ts pattern. Lazy require behind getSeaNative();
  capability-detection helper tryGetSeaNative(); structured error
  messages that classify MODULE_NOT_FOUND vs ERR_DLOPEN_FAILED and
  include platform/arch/Node-version + install hint.
- H4 supply chain: pin @napi-rs/cli to 2.18.4 in devDependencies;
  build:native switches to `npx --no-install` so the pinned local
  install is used (no per-build network fetch).
- H5 path: switch build:native default kernel path to the canonical
  `../../databricks-sql-kernel/napi-binding` (the worktree-specific
  `-sea-WT` suffix is gone).
- H6 CI safety: e2e suite hard-fails when CI=true and any required
  env var is missing; dev machines still skip.
- H8 runtime guard: loader throws a structured error on Node <18
  instead of attempting a dlopen that would fail mysteriously.
  Driver's engines.node stays >=14 — Thrift consumers on older
  Node continue to work.
- H9 + M1 + M2 types: tsconfig adds a `@sea-native` path alias to
  native/sea/index.d.ts; loader imports the real Connection /
  Statement / ConnectionOptions / ExecuteOptions / ArrowBatch /
  ArrowSchema types and re-exports them. Tests use the re-exported
  types — the inline-shape duplication across three files collapses.
- M3 README: rewrite native/sea/README.md to match kernel reality.
  The napi crate is a standalone Cargo workspace (not a pyo3
  sibling); explain the tls-rustls choice that the standalone
  workspace exists to enable.
- M4 drift detection: mark native/sea/index.d.ts as
  linguist-generated in .gitattributes so GitHub collapses it in
  diffs and excludes from blame/language stats.
- M5 artifacts: gitignore native/sea/index.js, index.node,
  index.*.node.
- M6 + M7 e2e coverage: decode the IPC payload via apache-arrow's
  tableFromIPC and assert numRows + cell value (not just shape);
  add drain-past-null idempotence and schema-before-fetch
  coverage.
- L1 build scripts: collapse build:native + build:native:debug
  into one script taking BUILD_PROFILE (defaults to --release).
- L2 / L4 / M8: drop the version() alias and the "Round 2+ will…"
  comment debt; the loader now actually delivers the value the
  prior JSDoc was only promising.

Verified on this branch: npm run lint clean (0 errors); npm run
prettier clean for PR-owned files (3 unrelated pre-existing
warnings on PR #378 territory); tsc --project tsconfig.build.json
clean; mocha on tests/unit/sea passes (4/4); mocha on
tests/e2e/sea passes against a live pecotesting warehouse (2/2,
IPC decode confirms SELECT 1 returns 1).

Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
Comment thread lib/DBSQLOperation.ts
disableBuffering: options?.disableBuffering,
});
const limit = options?.maxRows ?? defaultMaxRows;
const result = await this.backend.fetchChunk({ limit, disableBuffering: options?.disableBuffering });
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — behavioral regression on the Thrift hot path (verified against PR head SHA + main).

Pre-PR fetchChunkInternal sequence:

  1. failIfClosed()
  2. waitUntilReady()
  3. getResultHandler() ← may fetch result-metadata RPC
  4. failIfClosed() ← critical mid-flight check
  5. setTimeout(0) macrotask yield
  6. resultHandler.fetchNext() ← data RPC (CloudFetch URL download / Arrow batch)
  7. failIfClosed()

Post-PR sequence: steps 3–6 now happen inside ThriftOperationBackend.fetchChunk as one atomic block with no failIfClosed check between them. The only intervening macrotask yield (setTimeout(0) at lib/thrift-backend/ThriftOperationBackend.ts:143-145) sits inside that block.

A concurrent cancel()/close() arriving during the yield window will no longer short-circuit the subsequent fetchNext() data RPC. CloudFetch presigned-URL downloads can take seconds, making this observable rather than theoretical.

Suggested fix: add an isClosed: () => boolean callback to IOperationBackend.fetchChunk's options, and have ThriftOperationBackend.fetchChunk check it after setTimeout(0) returns. Alternatively split the metadata-prep phase into a separate backend method so the facade can re-check between them.

Found by: security, performance (independently).


Posted by code-review-squad · /full-review

* the operation into its closed/cancelled flags; future implementations must
* use the same error type for the facade to stay in sync.
*/
waitUntilReady(options?: WaitUntilReadyOptions): Promise<void>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — locks the cross-backend interface to a Thrift wire shape.

IOperationBackend.waitUntilReady(options?: WaitUntilReadyOptions) is the cross-backend interface, but WaitUntilReadyOptions (imported from ./IOperation) types callback as (progress: TGetOperationStatusResp) => unknown — a Thrift-generated wire type.

Every non-Thrift backend impl must either:

  • depend on the thrift IDL just to construct TGetOperationStatusResp, or
  • import synthesizeThriftStatus from lib/utils/thriftWireSynthesis.ts per-poll, or
  • skip the user's callback, silently breaking a public contract.

For a Rust kernel polling every ~100 ms during a 10-min query, that's 6,000 synthesized fake Thrift structs purely to satisfy a JS callback signature.

Suggested fix: introduce a separate IOperationBackendWaitOptions { callback?: (status: OperationStatus) => unknown } on the backend interface. DBSQLOperation owns the user's Thrift-typed callback and adapts to/from the neutral one at the facade boundary.

Easier to fix now (one caller, in this PR's own facade) than after the SEA/napi impls land.

Found by: architecture, devils-advocate, security, language.


Posted by code-review-squad · /full-review

*/
export function synthesizeThriftStatus(status: OperationStatus): TGetOperationStatusResp {
return {
status: synthesizeOkStatus(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — latent correctness bug; bites once any non-Thrift backend is wired in.

synthesizeOkStatus() (line 12) returns { statusCode: TStatusCode.SUCCESS_STATUS } as TStatus unconditionally, and is wired into the status field of the synthesized TGetOperationStatusResp regardless of OperationState.

Existing user code that calls Status.assert(resp.status) after await op.status() against a non-Thrift backend will see SUCCESS_STATUS even when the operation finished in ERROR_STATE or CANCELED_STATE. The lossy-by-design JSDoc comment covers fields like taskStatus/numModifiedRows but does NOT cover statusstatus is meant to reflect the RPC, but here it's being used as a stand-in for the operation outcome that user code already checks.

Today this is dead code (SEA is a throw-stub); the moment any non-Thrift backend's status() is reachable, error states will silently look successful in the legacy IOperation.status() return value.

Suggested fix: map operation-state to a real TStatus.statusCode:

  • OperationState.Failed → ERROR_STATUS (and carry errorMessage/sqlState)
  • OperationState.Cancelled → ERROR_STATUS
  • other states → SUCCESS_STATUS

Trivial, prevents a latent correctness gap.

Found by: agent-compat, devils-advocate.


Posted by code-review-squad · /full-review

@@ -0,0 +1,87 @@
import {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — coverage gap that masks the latent correctness bug in M2.

No test exercises client.connect({ useSEA: true }) end-to-end. The synthesize helpers in this file have no companion tests/unit/utils/thriftWireSynthesis.test.ts. Both DBSQLOperation.status() and DBSQLOperation.getMetadata() have the instanceof ThriftOperationBackend synthesize fallback as dead code from the test suite's perspective.

Any enum-mapping drift (e.g. the M2 issue above) would not be caught by CI.

Suggested fix: add a unit test file tests/unit/utils/thriftWireSynthesis.test.ts covering:

  • operationStateToThrift: all 7 enum values
  • resultFormatToThrift: all 3 enum values
  • synthesizeThriftStatus: confirm errorMessage/sqlState are preserved; confirm hasResultSet round-trips; confirm state: Failed ⇒ non-SUCCESS TStatus (after M2 fix)
  • synthesizeThriftResultSetMetadata: confirm schema, arrowSchema, isStagingOperation, lz4Compressed are preserved

And one new test in DBSQLClient.test.ts covering the useSEA: true failure path (using as any cast to bypass the InternalConnectionOptions non-exported type).

Found by: test, ops, agent-compat (independently flagged different aspects).


Posted by code-review-squad · /full-review

Comment thread lib/DBSQLClient.ts
private forwardConnectionEvent(event: 'error' | 'reconnecting' | 'close' | 'timeout', payload?: unknown): void {
switch (event) {
case 'error': {
const error = payload as Error;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — latent crash hazard. Pre-refactor the Thrift EventEmitter typing guaranteed error: Error. Post-refactor payload is unknown and the cast is unchecked. If a future backend ever emits a non-Error value through this path (e.g. a string, which EventEmitter.emit('error', ...) permits), error.stack || error.name + ': ' + error.message will throw TypeError. The try/catch only protects this.emit, not the logger.log line.

  • Suggested fix:
    const error = payload instanceof Error ? payload : new Error(String(payload));

Posted by code-review-squad · /full-review

Comment thread lib/DBSQLClient.ts
// doesn't ship in the public `.d.ts`. Mirrors Python's `kwargs.get("use_sea")`
// pattern (see databricks-sql-python/src/databricks/sql/session.py).
const internalOptions = options as ConnectionOptions & InternalConnectionOptions;
this.backend = internalOptions.useSEA
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — confusing diagnostic on a partially-initialized path.

With useSEA: true, SeaBackend.connect() always throws (M0 stub), but this.backend is already assigned. A subsequent openSession() passes the !this.backend guard at line 298 and reaches SeaBackend.openSession(), which throws "SEA backend not implemented yet" instead of "DBSQLClient: not connected".

  • Suggested fix: either (a) assign this.backend after a successful connect(), or (b) try/catch around await this.backend.connect(options) and null this.backend on failure.

The IBackend.close() JSDoc explicitly mentions "MUST be safe to call on a partially-initialized backend (i.e. after a failed connect())" — but that contract only covers close(), not the openSession-after-failed-connect path.


Posted by code-review-squad · /full-review

return TSparkRowSetType.ARROW_BASED_SET;
case ResultFormat.UrlBased:
return TSparkRowSetType.URL_BASED_SET;
default:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — asymmetric with operationStateToThrift above.

operationStateToThrift (lines 16-34) uses an explicit OperationState.Unknown member, making the switch exhaustively-checkable by TS. resultFormatToThrift (lines 36-47) just has a bare default: return COLUMN_BASED_SET. Today the default is dead because ResultFormat is a closed 3-value enum, but if it's extended for SEA (e.g. a new format), the fallback would silently route results through JsonResultHandler and produce garbled data with no error.

  • Suggested fix: drop the default, or throw new HiveDriverError(...). Match operationStateToThrift's pattern.

Posted by code-review-squad · /full-review

Comment thread lib/DBSQLOperation.ts
* fields (`cacheLookupResult`, `uncompressedBytes`, `compressedBytes`,
* `status`) left undefined / defaulted.
*
* Prefer {@link DBSQLOperation.getResultMetadata} in new code.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — agent/IDE discoverability.

The JSDoc says "Prefer DBSQLOperation.getResultMetadata in new code" but does NOT use the @deprecated tag. TypeScript's @deprecated is the canonical signal that IDEs (strikethrough), tsc, ESLint plugins, and agentic codegen pick up. Free-text "Prefer X" won't.

  • Suggested fix:
    /**
     * @deprecated Use {@link DBSQLOperation.getResultMetadata}; this method synthesizes Thrift-only fields as `undefined` on non-Thrift backends.
     */

Posted by code-review-squad · /full-review

Comment thread lib/DBSQLSession.ts

return result;
}
// Re-export for back-compat with existing imports.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — back-compat re-export should signal Thrift-only.

numberToInt64 traffics in node-int64 (a Thrift wire type) and has no place in a backend-neutral facade — it's intentionally re-exported only to preserve back-compat for existing external consumers. Add @deprecated to the re-export and a // Thrift-only JSDoc on the symbol itself, so SDK consumers see the deprecation in their IDE / .d.ts.


Posted by code-review-squad · /full-review

Comment thread lib/DBSQLSession.ts
@@ -211,48 +85,13 @@ export default class DBSQLSession implements IDBSQLSession {
*/
public async executeStatement(statement: string, options: ExecuteStatementOptions = {}): Promise<IOperation> {
await this.failIfClosed();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — readability / maintenance.

Pre-refactor, handleResponse did one job (re-check open-flag after the backend call returned, because server-side close doesn't error out). The PR removes the helper and inlines a failIfClosed bracket around every backend call — 11 sites with the same shape, no name attached to the "before AND after" semantics.

  • Suggested fix:
    private async runBackend<T>(fn: () => Promise<T>): Promise<T> {
      await this.failIfClosed();
      const result = await fn();
      await this.failIfClosed();
      return result;
    }
    Then call sites become return this.wrapOperation(await this.runBackend(() => this.backend.getCatalogs(request)));. Cuts boilerplate and re-attaches a name to the contract.

Posted by code-review-squad · /full-review

@vikrantpuppala
Copy link
Copy Markdown
Collaborator

Code Review Squad — Review

Score: 65/100 — HIGH RISK (score driven by finding count — see notes)

The structural refactor is well-scoped and well-documented; the Thrift path is preserved 1:1 for nearly every call site and the 3-tier seam (IBackend / ISessionBackend / IOperationBackend) is drawn at the right level for the upcoming Rust/N-API kernel backend. Reviewers reached strong agreement on this point.

The score is depressed by one verified behavioral regression on the Thrift hot path (H1 — fetchChunk lost a mid-flight failIfClosed between metadata RPC and data RPC), a cluster of four forward-looking architecture concerns that will hit the upcoming Rust kernel binding, and a long tail of small concerns. Several of the architecture concerns are already acknowledged in JSDoc on the PR. Treat the HIGH RISK label as "address H1 plus a couple of the Mediums before merge", not as a reject signal.

Reviewers: 9/9 delivered (security, devils-advocate, language, test, architecture, maintainability, agent-compat, ops, performance).


Posted inline


Medium Findings

M3 — Telemetry schema has no backend field — can't distinguish Thrift / SEA / Rust in metrics

File: lib/telemetry/types.ts (not in this PR's diff)

Medium — cheap to add now, expensive to backfill after rollout.

TelemetryEvent (lines 97-159), TelemetryMetric (lines 165-205), and DriverConfiguration (lines 208-280) have no backend?: 'thrift' | 'sea' | 'kernel' field. The driver-config carries driverName, driverVersion, nodeVersion, cloudFetchEnabled, arrowEnabled, directResultsEnabled — but no backend identifier.

Once SEA starts emitting telemetry, every STATEMENT_COMPLETE from a SEA path will be indistinguishable from a Thrift path. Slicing latency / error rate / cloudfetch effectiveness by backend will require a metrics-schema change + redeploy. Triaging "is the new backend regressing latency vs Thrift?" during the rollout — impossible from telemetry alone.

  • Suggested fix: add backend?: 'thrift' | 'sea' | 'kernel' (or similar) to TelemetryEvent and to DriverConfiguration in a follow-up PR before any non-Thrift implementation is wired into emission. Cheap additive change.

Found by: ops.

Low Findings

Low findings — click to expand

L7 — Three why-comments deleted in DBSQLSession (staging detection, AWS/Azure 404, Content-Length)

The PR collapsed three multi-line why-comments down to single lines or removed them:

  • Staging-detection invariant in executeStatement (~line 91 post-PR): the original 6-line comment explained that calling getResultMetadata() here for a non-staging operation is intentional and harmless because the operation remains usable.
  • AWS vs Azure 404 behavior on staging-remove (response.status !== 404 check).
  • Required Content-Length header on staging-upload.

These are exactly the why-comments to preserve — they document non-obvious decisions. Restore.

L8 — hasResultSet: readonly boolean on IOperationBackend is misleading (mutable getter under the hood)

4 reviewers flagged this independently (language, agent-compat, devils-advocate, architecture).

lib/contracts/IOperationBackend.ts:18-23 declares readonly hasResultSet: boolean with a long JSDoc explaining that readonly means "external callers cannot reassign," not "fixed at construction time," and that implementations MUST refresh from terminal status responses.

A fresh implementer (especially the future Rust/N-API backend author) will see readonly and assume constructor-time const. Either:

  • make it a method hasResultSet(): boolean (clearly state-dependent), or
  • leave a more visible "this is a getter" comment that the napi-rs binding author will catch.

Cost of method form: ~5 call sites in lib/DBSQLOperation.ts.

L9 — thriftWireSynthesis.ts should live under lib/thrift-backend/ not lib/utils/

The file imports 6 symbols from thrift/TCLIService_types and produces Thrift-typed values. By the same logic that moved numberToInt64 / getDirectResultsOptions into lib/thrift-backend/, this file belongs there too. lib/utils/ signals "shared by everyone," which puts a Thrift-aware module on the dependency graph of any future neutral helper. When the Rust kernel lands, this file should not be its dependency.

  • Suggested fix: rename to lib/thrift-backend/wireSynthesis.ts.

L10 — instanceof ThriftOperationBackend downcast in 2 facade methods — defensible exemption, but should not become a pattern

DBSQLOperation.status (line 147) and DBSQLOperation.getMetadata (line 235) both instanceof ThriftOperationBackend to pick a zero-loss fast path. The doc comments on the backend's thriftStatusResponse / thriftResultMetadataResponse explicitly justify this as a back-compat exemption.

The risk is precedent: when the Rust backend lands, somebody will add instanceof RustOperationBackend for the same reason, and the facade grows three branches per public method. The cleaner long-term path is an optional method on IOperationBackend (thriftStatusResponse?(): Promise<TGetOperationStatusResp>) or a structural 'thriftStatusResponse' in this.backend check.

Suggested action: document the rule "no further instanceof <BackendClass> downcasts in the facade" in CLAUDE.md or in the contributing notes. The two existing ones are exempt by back-compat.

L11 — Backend identifier missing from log messages

With three backends coming (Thrift, SEA, Rust kernel), every facade-level debug log (e.g. Operation created with id: <guid>, Session created with id: <guid>, Fetched chunk of size: <n> from operation with id: <guid>) is backend-agnostic. When a customer reports "session abcd-1234 hung", on-call has no log-level signal whether to look at Thrift server logs, SEA control-plane logs, or kernel-process logs. The id space may also overlap or collide between backends.

  • Suggested fix: add readonly kind: 'thrift' | 'sea' | 'kernel' (or name: string) to IBackend/ISessionBackend/IOperationBackend, and prefix facade-level logs with [${this.backend.kind}]. ~7 log call sites + one field per interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants